﻿using System;
using System.Collections.Generic;
using System.ServiceModel;
using CentralService.TranslatorService;
using Contracts;

namespace CentralService
{
    // Como é necessário enviar mensagens para todos os clientes ligados, o serviço tem que ter modo de instanciação Single, para que não seja necessário 
    //  recorrer a outra forma de guardar a informação sobre os clientes ligados.
    // Pensar em alterar o ConcurrencyMode.Single. Da forma como está, não deve permitir que diferentes clientes se liguem ao serviço
    //  para efectuar operações diferentes.
    // Com ConcurrencyMode.Reentrant é possível perder o lock(na operação DeliverMessage, por exemplo) para invocar a operação de ReceiveMessage do
    //  clientProxy actual, impedindo que outros clientes executem operações. Com ConcurrencyMode.Single não era possível, retornava excepção.
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Reentrant)]
    public class CentralService : ICentralService
    {
        // proxy para o serviço de tradução
        private readonly LanguageServiceClient _svc;

        // Key = subject
        // Value = Key=proxy, Value=language
        private readonly IDictionary<string, IDictionary<ICService, string>> _onlineUsers = new Dictionary<string, IDictionary<ICService, string>>();

        public CentralService()
        {
            _svc = new LanguageServiceClient();
        }

        // Apesar de não ser de grande utilidade na implementação do serviço, decidimos utilizar um parâmetro
        //  deste tipo(UserData) para permitir utilizar o atributo DataContract.
        public void InitSubscription(UserData subjectAndLanguage)
        {
            // Aceder ao contexto da sessão definida pelo cliente
            //string clientName = GetClientNameFromContext();
            string sessionId = OperationContext.Current.SessionId;
            // Proxy para o cliente. Relacionado com o clientName encontrado a partir do contexto da sessão.
            ICService clientProxy = OperationContext.Current.GetCallbackChannel<ICService>();

            IDictionary<ICService, string> value;
            if (!_onlineUsers.TryGetValue(subjectAndLanguage.Subject, out value))
            {
                _onlineUsers[subjectAndLanguage.Subject] = new Dictionary<ICService, string>();
            }

            _onlineUsers[subjectAndLanguage.Subject].Add(clientProxy, subjectAndLanguage.Language);

            Console.WriteLine("InitSubscription => Subject: {0}, Language: {1}, SessionID: {2}",
                              subjectAndLanguage.Subject, subjectAndLanguage.Language, sessionId);
        }

        public void ExitSubscription(string subject)
        {
            // Aceder ao contexto da sessão definida pelo cliente
            //string clientName = GetClientNameFromContext();
            string sessionId = OperationContext.Current.SessionId;
            // Proxy para o cliente. Relacionado com o clientName encontrado a partir do contexto da sessão.
            ICService clientProxy = OperationContext.Current.GetCallbackChannel<ICService>();

            IDictionary<ICService, string> value;
            if (!_onlineUsers.TryGetValue(subject, out value))
            {
                value.Remove(clientProxy);
            }

            Console.WriteLine("ExitSubscription => Subject: {0}, SessionID: {1}", subject, sessionId);
        }

        public void DeliverMessage(string language, string subject, string message)
        {
            // Aceder ao contexto da sessão definida pelo cliente
            //string clientName = GetClientNameFromContext();
            string sessionId = OperationContext.Current.SessionId;
            // Proxy para o cliente que quer entregar mensagem.
            ICService clientProxy = OperationContext.Current.GetCallbackChannel<ICService>();

            Console.WriteLine("DeliverMessage => SessionID: {0}\nSubject: {1}\nLanguage: {2}\nMessage: {3}", sessionId, subject, language, message);

            // Key = language, Value = mensagem traduzida
            // Assim reduz-se o número de chamdas remotas ao serviço de tradução, se existirem clientes com a mesma linguagem.
            IDictionary<string, string> translations = new Dictionary<string, string>();
            // Esta colecção serve para guardar todos os users que, durante a entrega da mensagem traduzida, não foi possível contactar, efectuar a chamada remota.
            // A remoção não é feita logo quando detectado porque a colecção de utilizadores ligados ficaria inconsistente e acabaria por não ser percorrida na sua totalidade.
            // Mais uma razão para que o ConcurrencyMode seja diferente de Multiple.
            LinkedList<ICService> toRemove = new LinkedList<ICService>();

            int successfulDeliveries = 0;
            foreach (var usersForSubject in _onlineUsers[subject])
            {
                if (usersForSubject.Key.Equals(clientProxy))
                    continue;

                string translatedMessage = null;
                string targetLanguage = usersForSubject.Value;
                bool existsTranslation = false;
                
                try
                {
                    if (language.Equals(targetLanguage))
                        translatedMessage = message;
                    else if (!(existsTranslation = translations.TryGetValue(targetLanguage, out translatedMessage)))
                        translatedMessage = _svc.Translate("F4E6E0444F32B660BED9908E9744594B53D2E864", message, language,
                                          targetLanguage, "text/html", "general");
                } catch(Exception) // tentar traduzir outra vez, pode ter sido um problema momentâneo
                {
                    try
                    {
                        if (language.Equals(targetLanguage))
                            translatedMessage = message;
                        else
                            translatedMessage = _svc.Translate("F4E6E0444F32B660BED9908E9744594B53D2E864", message, language,
                                          targetLanguage, "text/html", "general");
                    }
                    catch (Exception)
                    {
                        translatedMessage = message;
                    }
                }
                finally
                {
                    try
                    {
                        if (!language.Equals(targetLanguage) && !existsTranslation && !translatedMessage.Equals(message))
                        {
                            // Adicionar mensagem traduzida à lista de traduções para a mensagem corrente.
                            translations[targetLanguage] = translatedMessage;
                        }

                        usersForSubject.Key.ReceiveMessage(translatedMessage);
                        successfulDeliveries++;
                        Console.WriteLine("Mensagem '{0}' entregue a um cliente.", translatedMessage);
                    }
                    catch (Exception)
                    {
                        toRemove.AddLast(usersForSubject.Key);
                        // retirar esta referência que não funcionou da colecção de online users.
                        //_onlineUsers[subject].Remove(usersForSubject.Key); // Desta forma a colecção fica inconsistente.
                        Console.WriteLine("Removido cliente que não foi possível contactar.");
                    }
                }
            }
            try
            {
                // Notificar cliente que a mensagem foi entregue
                clientProxy.ReceiveMessage(string.Format("Mensagem '{0}' entregue com sucesso a {1} utilizador(es).", message, successfulDeliveries));
            } catch(Exception e)
            {
                toRemove.AddLast(clientProxy);
                // retirar esta referência que não funcionou da colecção de online users.
                //_onlineUsers[subject].Remove(clientProxy);
                Console.WriteLine("Não foi possível notificar o cliente da entrega da mensagem.\n{0}", e.Message);
            }
            finally
            {
                foreach (var proxy in toRemove)
                {
                    _onlineUsers[subject].Remove(proxy);
                }
            }
        }

        //private static string GetClientNameFromContext()
        //{
        //    MessageProperties msgProp = OperationContext.Current.IncomingMessageProperties;
        //    ContextMessageProperty ctxProperty = msgProp[ContextMessageProperty.Name] as ContextMessageProperty;

        //    if (ctxProperty == null)
        //        throw new ArgumentNullException("Não foi possível criar a mensagem com a informação do contexto de sessão!");

        //    if (ctxProperty.Context.ContainsKey("Name"))
        //    {
        //        return ctxProperty.Context["Name"];
        //    }

        //    throw new ArgumentNullException("Não foi possível obter o nome do cliente associado à sessão actual!");
        //}
    }
}
